package com.joinway.framework.extension.net.http;

import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.HttpMessage;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.ParseException;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.entity.AbstractHttpEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.joinway.framework.extension.net.json.JsonConverter;
import com.joinway.framework.extension.net.util.NetUtils;

public class RestHttpClient {

	private final static Logger log = LoggerFactory.getLogger(RestHttpClient.class);
	
	private static final String JSON_TYPE = "application/json";
	
	protected Map<String, String> headers;
	
	protected HttpClient client;
	
	public RestHttpClient(){
		client = HttpClients.createDefault();
	}
	
	/**
	 * 
	 * @param key	header key
	 * @param value	header value
	 */
	public RestHttpClient(String key, String value){
		this();
		headers = new LinkedHashMap<>();
		headers.put(key, value);
	}
	
	public RestHttpClient(Map<String, String> headers){
		this();
		this.headers = new LinkedHashMap<>(headers);
	}
	
	public static Map<String, String> convert(List<NameValuePair> params){
		Map<String, String> map = new LinkedHashMap<>();
		
		if(params != null && params.size() > 0){
			for(NameValuePair nvp : params){
				map.put(nvp.getName(), nvp.getValue());
			}
		}
		
		return map;
	}
	
	public String get(String url) throws ClientProtocolException, IOException  {
		return get(url, null, String.class);
	}
	
	public String get(String url, Map<String, String> params) throws ClientProtocolException, IOException {
		return get(url, params, String.class);
	}

	public<O> O get(String url, Class<O> out) throws ClientProtocolException, IOException  {
		return get(url, null, out);
	}

	public<O> void get(final String url, final Class<O> out, final RestHttpClientCallBack<O> callback) {
		get(url, null, out, callback);
	}

	@SuppressWarnings("unchecked")
	public<O> O get(String url, Map<String, String> params, Class<O> out) throws ClientProtocolException, IOException {
		List<NameValuePair> pair = NetUtils.buildNameValuePair(params);
		
		HttpGet get;
		
		if(pair == null){
			get = new HttpGet(url);
		}else{
			get = new HttpGet(url + "?" + URLEncodedUtils.format(pair, Consts.UTF_8));
		}
		
		addHeader(get, headers);
		
		HttpResponse response = client.execute(get);
		
		HttpEntity entity = response.getEntity();
	    String text = EntityUtils.toString(entity);
	    
	    O output;
	    
	    if(out == String.class){
	    	output = (O)String.valueOf(text);
	    }else{
	    	output = JsonConverter.jsonToObject(text, out);
	    }
	    
	    return output;
	}
	
	public<O, T> void get(final String url, final Map<String, String> params, final Class<O> out, final RestHttpClientCallBack<O> callback) {
		Thread thread = new Thread(new Runnable(){
			@Override
			public void run() {
				try {
					O output = get(url, params, out);
					callback.process(output);
				} catch (Exception e) {
					log.error("failed to execute get request " + url, e);
				}
			}
		});
		
		thread.start();
	}

	public String delete(String url) throws ClientProtocolException, IOException  {
		return delete(url, null, String.class);
	}
	
	public String delete(String url, Map<String, String> params) throws ClientProtocolException, IOException {
		return delete(url, params, String.class);
	}

	public<O> O delete(String url, Class<O> out) throws ClientProtocolException, IOException {
		return delete(url, null, out);
	}

	public<O> void delete(final String url, final Class<O> out, final RestHttpClientCallBack<O> callback) {
		delete(url, null, out, callback);
	}

	@SuppressWarnings("unchecked")
	public<O> O delete(String url, Map<String, String> params, Class<O> out) throws ClientProtocolException, IOException {
		List<NameValuePair> pair = NetUtils.buildNameValuePair(params);
		
		HttpDelete delete;
		
		if(pair == null){
			delete = new HttpDelete(url);
		}else{
			delete = new HttpDelete(url + "?" + URLEncodedUtils.format(pair, Consts.UTF_8));
		}
		
		addHeader(delete, headers);
		
		HttpResponse response = client.execute(delete);
		
		HttpEntity entity = response.getEntity();
	    String text = EntityUtils.toString(entity);
	    
	    O output;
	    
	    if(out == String.class){
	    	output = (O)String.valueOf(text);
	    }else{
	    	output = JsonConverter.jsonToObject(text, out);
	    }
	    
	    return output;
	}
	
	public<O, T> void delete(final String url, final Map<String, String> params, final Class<O> out, final RestHttpClientCallBack<O> callback) {
		Thread thread = new Thread(new Runnable(){
			@Override
			public void run() {
				try {
					O output = delete(url, params, out);
					callback.process(output);
				} catch (Exception e) {
					log.error("failed to execute delete request " + url, e);
				}
			}
		});
		
		thread.start();
	}

	public String post(String url) throws ClientProtocolException, IOException {
		return post(url, null, String.class);
	}
	
	public<I> String post(String url, I input) throws ClientProtocolException, IOException {
		return post(url, input, String.class);
	}

//	public String post(String url, List<NameValuePair> params) throws Exception {
//		return post(url, params, String.class);
//	}

	public String post(String url, Map<String, String> params) throws ClientProtocolException, IOException {
		return post(url, params, Consts.UTF_8.toString(), String.class);
	}

	public String post(String url, Map<String, String> params, String encode) throws ClientProtocolException, IOException {
		return post(url, params, encode, String.class);
	}
	
	public<O> O post(String url, Class<O> out) throws ClientProtocolException, IOException {
		return post(url, null, Consts.UTF_8.toString(), out);
	}

	public<O> void post(final String url, final Map<String, String> params, final String encode, final Class<O> out, final RestHttpClientCallBack<O> callback) {
		Thread thread = new Thread(new Runnable(){
			@Override
			public void run() {
				try {
					O output = post(url, params, encode, out);
					callback.process(output);
				} catch (Exception e) {
					log.error("failed to execute post request " + url, e);
				}
			}
		});
		
		thread.start();
	}

	public<O> void post(final String url, final Map<String, String> params, final Class<O> out, final RestHttpClientCallBack<O> callback) {
		post(url, params, Consts.UTF_8.toString(), out, callback);
	}


	public<O> O post(String url, Map<String, String> params, Class<O> out) throws ClientProtocolException, IOException {
	    return post(url, params, Consts.UTF_8.toString(), out);
	}

	public<O> O post(String url, Map<String, String> params, String encode, Class<O> out) throws ClientProtocolException, IOException {
		List<NameValuePair> pair = NetUtils.buildNameValuePair(params);
		
		HttpPost post = new HttpPost(url);
		
		if(pair != null){
			AbstractHttpEntity entity = new UrlEncodedFormEntity(pair, encode);
			initEntity(entity, encode);
			post.setEntity(entity);
		}
		
	    return doPost(post, out);
	}

	public<I, O> O post(String url, I input, Class<O> out) throws ClientProtocolException, IOException {
		HttpPost post = new HttpPost(url);
		post.addHeader(HTTP.CONTENT_TYPE, JSON_TYPE);
		
		if(input != null){
			AbstractHttpEntity entity = new StringEntity(JsonConverter.objectToJson(input));
			initEntity(entity, Consts.UTF_8.toString());
			post.setEntity(entity);
		}
	    
	    return doPost(post, out);
	}
	
	public<I, O> void post(final String url, final I input, final Class<O> out, final RestHttpClientCallBack<O> callback) {
		Thread thread = new Thread(new Runnable(){
			@Override
			public void run() {
				try {
					O output = post(url, input, out);
					callback.process(output);
				} catch (Exception e) {
					log.error("failed to execute post request " + url, e);
				}
			}
		});
		
		thread.start();
	}
	
	public String put(String url) throws ParseException, IOException {
		return put(url, null, String.class);
	}
	
	public<I> String put(String url, I input) throws ParseException, IOException {
		return put(url, input, String.class);
	}
	
	public String put(String url, Map<String, String> params) throws ParseException, IOException {
		return put(url, params, String.class);
	}
	
	public<O> O put(String url, Class<O> out) throws ParseException, IOException {
		return put(url, null, out);
	}

	public<O> void put(final String url, final Map<String, String> params, final Class<O> out, final RestHttpClientCallBack<O> callback) {
		Thread thread = new Thread(new Runnable(){
			@Override
			public void run() {
				try {
					O output = put(url, params, out);
					callback.process(output);
				} catch (Exception e) {
					log.error("failed to execute put request " + url, e);
				}
			}
		});
		
		thread.start();
	}

	public<O> O put(String url, Map<String, String> params, Class<O> out) throws ParseException, IOException {
	    return put(url, params, Consts.UTF_8.toString(), out);
	}

	public<O> O put(String url, Map<String, String> params, String encode, Class<O> out) throws ParseException, IOException {
		List<NameValuePair> pair = NetUtils.buildNameValuePair(params);
		
		HttpPut put = new HttpPut(url);
		
		if(pair != null){
			AbstractHttpEntity entity = new UrlEncodedFormEntity(pair, encode);
			initEntity(entity, encode);
			put.setEntity(entity);
		}
		
	    return doPut(put, out);
	}

	public<I, O> O put(String url, I input, Class<O> out) throws ParseException, IOException {
		HttpPut put = new HttpPut(url);
		put.addHeader(HTTP.CONTENT_TYPE, JSON_TYPE);
		
		if(input != null){
			AbstractHttpEntity entity = new StringEntity(JsonConverter.objectToJson(input));
			initEntity(entity, Consts.UTF_8.toString());
			put.setEntity(entity);
		}
	    
	    return doPut(put, out);
	}
	
	public<I, O> void put(final String url, final I input, final Class<O> out, final RestHttpClientCallBack<O> callback) {
		Thread thread = new Thread(new Runnable(){
			@Override
			public void run() {
				try {
					O output = put(url, input, out);
					callback.process(output);
				} catch (Exception e) {
					log.error("failed to put post request " + url, e);
				}
			}
		});
		
		thread.start();
	}
	
	protected void initEntity(AbstractHttpEntity entity, String type, String encode){
		entity.setContentEncoding(encode);
		
		if(type != null && type.trim().length() > 0){
			entity.setContentType(type);
		}
	}
	
	protected void initEntity(AbstractHttpEntity entity, String encode){
		initEntity(entity, null, encode);
	}
	
	@SuppressWarnings("unchecked")
	protected<O> O doPost(HttpPost post, Class<O> out) throws IOException {
		addHeader(post, headers);
		
		HttpResponse response = client.execute(post);
		
		HttpEntity entity = response.getEntity();
	    String text = EntityUtils.toString(entity);
	    
	    O output;
	    
	    if(out == String.class){
	    	output = (O)String.valueOf(text);
	    }else{
	    	output = JsonConverter.jsonToObject(text, out);
	    }
	    
	    return output;
	}
	
	@SuppressWarnings("unchecked")
	protected<O> O doPut(HttpPut put, Class<O> out) throws IOException {
		addHeader(put, headers);
		
		HttpResponse response = client.execute(put);
		
		HttpEntity entity = response.getEntity();
	    String text = EntityUtils.toString(entity);
	    
	    O output;
	    
	    if(out == String.class){
	    	output = (O)String.valueOf(text);
	    }else{
	    	output = JsonConverter.jsonToObject(text, out);
	    }
	    
	    return output;
	}
	
	protected void addHeader(HttpMessage msg, Map<String, String> headers){
	    if(headers != null && headers.size() > 0){
	    	Iterator<String> keys = headers.keySet().iterator();
	    	while(keys.hasNext()){
	    		String key = String.valueOf(keys.next());
	    		String value = String.valueOf(headers.get(key));
	    		
	    		msg.addHeader(key, value);
	    	}
	    }
	}
}
